// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;

namespace Microsoft.AspNetCore.Mvc.ModelBinding;

/// <summary>
/// A metadata object representing a source of data for model binding.
/// </summary>
[DebuggerDisplay("Source = {DisplayName}")]
public class BindingSource : IEquatable<BindingSource?>
{
    /// <summary>
    /// A <see cref="BindingSource"/> for the request body.
    /// </summary>
    public static readonly BindingSource Body = new BindingSource(
        "Body",
        Resources.BindingSource_Body,
        isGreedy: true,
        isFromRequest: true);

    /// <summary>
    /// A <see cref="BindingSource"/> for a custom model binder (unknown data source).
    /// </summary>
    public static readonly BindingSource Custom = new BindingSource(
        "Custom",
        Resources.BindingSource_Custom,
        isGreedy: true,
        isFromRequest: true);

    /// <summary>
    /// A <see cref="BindingSource"/> for the request form-data.
    /// </summary>
    public static readonly BindingSource Form = new BindingSource(
        "Form",
        Resources.BindingSource_Form,
        isGreedy: false,
        isFromRequest: true);

    /// <summary>
    /// A <see cref="BindingSource"/> for the request headers.
    /// </summary>
    public static readonly BindingSource Header = new BindingSource(
        "Header",
        Resources.BindingSource_Header,
        isGreedy: true,
        isFromRequest: true);

    /// <summary>
    /// A <see cref="BindingSource"/> for model binding. Includes form-data, query-string
    /// and route data from the request.
    /// </summary>
    public static readonly BindingSource ModelBinding = new BindingSource(
        "ModelBinding",
        Resources.BindingSource_ModelBinding,
        isGreedy: false,
        isFromRequest: true);

    /// <summary>
    /// A <see cref="BindingSource"/> for the request url path.
    /// </summary>
    public static readonly BindingSource Path = new BindingSource(
        "Path",
        Resources.BindingSource_Path,
        isGreedy: false,
        isFromRequest: true);

    /// <summary>
    /// A <see cref="BindingSource"/> for the request query-string.
    /// </summary>
    public static readonly BindingSource Query = new BindingSource(
        "Query",
        Resources.BindingSource_Query,
        isGreedy: false,
        isFromRequest: true);

    /// <summary>
    /// A <see cref="BindingSource"/> for request services.
    /// </summary>
    public static readonly BindingSource Services = new BindingSource(
        "Services",
        Resources.BindingSource_Services,
        isGreedy: true,
        isFromRequest: false);

    /// <summary>
    /// A <see cref="BindingSource"/> for special parameter types that are not user input.
    /// </summary>
    public static readonly BindingSource Special = new BindingSource(
        "Special",
        Resources.BindingSource_Special,
        isGreedy: true,
        isFromRequest: false);

    /// <summary>
    /// A <see cref="BindingSource"/> for <see cref="IFormFile"/>, <see cref="IFormCollection"/>, and <see cref="IFormFileCollection"/>.
    /// </summary>
    public static readonly BindingSource FormFile = new BindingSource(
        "FormFile",
        Resources.BindingSource_FormFile,
        isGreedy: true,
        isFromRequest: true);

    /// <summary>
    /// Creates a new <see cref="BindingSource"/>.
    /// </summary>
    /// <param name="id">The id, a unique identifier.</param>
    /// <param name="displayName">The display name.</param>
    /// <param name="isGreedy">A value indicating whether or not the source is greedy.</param>
    /// <param name="isFromRequest">
    /// A value indicating whether or not the data comes from the HTTP request.
    /// </param>
    public BindingSource(string id, string displayName, bool isGreedy, bool isFromRequest)
    {
        ArgumentNullException.ThrowIfNull(id);

        Id = id;
        DisplayName = displayName;
        IsGreedy = isGreedy;
        IsFromRequest = isFromRequest;
    }

    /// <summary>
    /// Gets the display name for the source.
    /// </summary>
    public string DisplayName { get; }

    /// <summary>
    /// Gets the unique identifier for the source. Sources are compared based on their Id.
    /// </summary>
    public string Id { get; }

    /// <summary>
    /// Gets a value indicating whether or not a source is greedy. A greedy source will bind a model in
    /// a single operation, and will not decompose the model into sub-properties.
    /// </summary>
    /// <remarks>
    /// <para>
    /// For sources based on a <see cref="IValueProvider"/>, setting <see cref="IsGreedy"/> to <c>false</c>
    /// will most closely describe the behavior. This value is used inside the default model binders to
    /// determine whether or not to attempt to bind properties of a model.
    /// </para>
    /// <para>
    /// Set <see cref="IsGreedy"/> to <c>true</c> for most custom <see cref="IModelBinder"/> implementations.
    /// </para>
    /// <para>
    /// If a source represents an <see cref="IModelBinder"/> which will recursively traverse a model's properties
    /// and bind them individually using <see cref="IValueProvider"/>, then set <see cref="IsGreedy"/> to
    /// <c>true</c>.
    /// </para>
    /// </remarks>
    public bool IsGreedy { get; }

    /// <summary>
    /// Gets a value indicating whether or not the binding source uses input from the current HTTP request.
    /// </summary>
    /// <remarks>
    /// Some sources (like <see cref="BindingSource.Services"/>) are based on application state and not user
    /// input. These are excluded by default from ApiExplorer diagnostics.
    /// </remarks>
    public bool IsFromRequest { get; }

    /// <summary>
    /// Gets a value indicating whether or not the <see cref="BindingSource"/> can accept
    /// data from <paramref name="bindingSource"/>.
    /// </summary>
    /// <param name="bindingSource">The <see cref="BindingSource"/> to consider as input.</param>
    /// <returns><c>True</c> if the source is compatible, otherwise <c>false</c>.</returns>
    /// <remarks>
    /// When using this method, it is expected that the left-hand-side is metadata specified
    /// on a property or parameter for model binding, and the right hand side is a source of
    /// data used by a model binder or value provider.
    ///
    /// This distinction is important as the left-hand-side may be a composite, but the right
    /// may not.
    /// </remarks>
    public virtual bool CanAcceptDataFrom(BindingSource bindingSource)
    {
        ArgumentNullException.ThrowIfNull(bindingSource);

        if (bindingSource is CompositeBindingSource)
        {
            var message = Resources.FormatBindingSource_CannotBeComposite(
                bindingSource.DisplayName,
                nameof(CanAcceptDataFrom));
            throw new ArgumentException(message, nameof(bindingSource));
        }

        if (this == bindingSource)
        {
            return true;
        }

        if (this == ModelBinding)
        {
            return bindingSource == Form || bindingSource == Path || bindingSource == Query;
        }

        return false;
    }

    /// <inheritdoc />
    public bool Equals(BindingSource? other)
    {
        return string.Equals(other?.Id, Id, StringComparison.Ordinal);
    }

    /// <inheritdoc />
    public override bool Equals(object? obj)
    {
        return Equals(obj as BindingSource);
    }

    /// <inheritdoc />
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }

    /// <inheritdoc />
    public static bool operator ==(BindingSource? s1, BindingSource? s2)
    {
        if (s1 is null)
        {
            return s2 is null;
        }

        return s1.Equals(s2);
    }

    /// <inheritdoc />
    public static bool operator !=(BindingSource? s1, BindingSource? s2)
    {
        return !(s1 == s2);
    }
}
